@use 'sass:list';
@use 'sass:map';
@use 'sass:meta';
@use 'sass:selector';
@use 'sass:string';
@use 'config';
@use 'size';
@use 'mixins';

$default-prefix: 'u';
$default-override: '';
$default-delimiter: '-';
$default-variant-delimiter: '\\:';

/*
    Main utility for generating utility classes with different viewports, delimiters, base class names, etc.
    Example: .sm\:hover\:u-text-blue:hover { ... }

    @class-prefix {string} [config.$utility-prefix] - prefix used for generated classes. This is the first section of the class name. Can be empty.
    @delimiter {string} [config.$delimiter] - delimiter used in class name body but not for separating psuedos. Can be empty.
    @base-class-name {string} - the root of the class name. For the utility class above, 'blue' is the base class name
    @class-value-pairs {map<string, any>[]} - list of mappings that maps the variant name (e.g. 'blue') to a map of CSS properties to values
    @variants {string[]} [()] - list of strings specifying which variants to generate styles for. Possible values:
        - 'responsive',
        - 'dark', 'light',
        - 'reduce-motion',
        - 'first-of-type',
        - 'last-of-type',
        - 'portrait', 'landscape',
        - 'hover', 'group-hover',
        - 'focus', 'group-focus', 'focus-visible', 'focus-within',
        - 'active',
        - 'visited',
        - 'checked',
        - 'disabled'

    @variant-delimiter {string} [config.$variant-delimiter] - delimiter used to separate the variant portion of the class. Can be empty but not advisable.
    @override {string} [config.$override] - override for CSS properties, like '!important'
*/
@mixin utility(
    $class-prefix: config.$utility-prefix,
    $delimiter: config.$delimiter,
    $base-class-name,
    $class-value-pairs,
    $variants: (),
    $variant-delimiter: config.$variant-delimiter,
    $override: config.$override
) {
    $config: (
        class-prefix: $class-prefix,
        delimiter: $delimiter,
        variant-delimiter: $variant-delimiter,
        override: $override,
    );
    @include generate-rules-pseudos-selectors(
            $config: $config,
            $variants: $variants,
            $context: (),
        )
        using ($props...) {
        @include kv-class-generator($base-class-name, $class-value-pairs, $props...) using ($key, $value, $props...) {
            // Set $props... so we don't crash in the event of extra params (can happen due to very recursive nature of this generator)
            @include mixins.explode-properties($value, $override);
        }
    }
}

@mixin generate-rules-pseudos-selectors($config, $variants: (), $context: ()) {
    $rules: (); // Media queries, after responsive classes
    $pseudos: (); // Selectors like :focus

    // Iterate over all variants and split between rules and pseudos
    @each $variant in $variants {
        // Check for get-rules
        @if meta.function-exists('get-#{$variant}') {
            $func: meta.get-function('get-#{$variant}');
            $rule-map: meta.call($func);
            $rules: map.set($rules, $variant, $rule-map); // ( 'dark': '@media (prefers-color-scheme: dark)',)
        } @else {
            $pseudos: list.append($pseudos, $variant);
        }
    }

    // Generate classes without rules
    @include generate-variants($config, $pseudos, $context) using ($props...) {
        @content ($props...);
    }

    // Generate with rules
    // Rules for responsiveness and light/dark mode are roots of separate classes, they never appear together
    @each $rule in map.values($rules) {
        @each $key, $value in $rule {
            $current-context: map.merge(
                $context,
                (
                    rule: $key,
                )
            );

            @if string.index($value, '@media') {
                $value: string.slice($value, 7);

                @media #{$value} {
                    @include generate-variants($config, $pseudos, $current-context) using ($props...) {
                        @content ($props...);
                    }
                }
            }
        }
    }
}

@mixin generate-variants($config, $variants: (), $context: ()) {
    // Get classes without pseudos, only rules
    $base-class-name: map.get($context, base-class-name);
    @if $base-class-name != null {
        // There is a parent selector. This is true if we are generating classes for the following scenarios:
        //  - Classes with .group as parent
        //  - Classes that have the main class name constructed. This is a recursive call to add in all the rules and variants to the class naming (e.g. text-blue and we want to add 'md:' and ':hover')
        @include get-base-class($config, $context) using ($props...) {
            @content ($props...);
        }

        @each $variant in $variants {
            $current-context: map.merge(
                $context,
                (
                    variant: $variant,
                    pseudo: $variant,
                )
            );
            @if string.index($variant, 'group-') {
                $current-context: map.merge(
                    $current-context,
                    (
                        pseudo: string.slice($variant, 7),
                        scope: '.group',
                    )
                );
            }

            @include get-base-class($config, $current-context) using ($props...) {
                @content ($props...);
            }
        }
    } @else {
        @content ($config, $variants, $context);
    }
}

@mixin get-base-class($config, $context) {
    $base: ();
    $delimiter: map.get(
        $map: $config,
        $key: delimiter,
    );
    $class-prefix: map.get($config, class-prefix);
    $base-class-name: map.get($context, base-class-name);

    @if $base-class-name != null {
        // Required to be comma separated since it will be used in generate-class() and treated like a selector

        $new-base: '';
        @if $class-prefix != null and string.length($class-prefix) > 0 {
            // If there is a prefix defined, use it in the class name
            $new-base: $class-prefix + $delimiter;
        }

        $new-base: $new-base + $base-class-name;
        $base: list.append($base, $new-base, comma);
    }

    @include generate-class($config, $base, $context...) using ($props...) {
        @content ($props...);
    }
}

@mixin generate-class($config, $base, $rule: null, $variant: null, $pseudo: null, $scope: null, $base-class-name: '') {
    $variant-delimiter: map.get(
        $map: $config,
        $key: variant-delimiter,
    );
    @if $variant {
        $base: selector.append('#{$variant}#{$variant-delimiter}', $base);
    }
    @if $rule {
        $base: selector.append('#{$rule}#{$variant-delimiter}', $base);
    }

    @if $scope and $pseudo {
        // For group related variants (e.g. .group:hover)
        $parent-selector: selector.append($scope, ':#{$pseudo}');
        $base: selector.nest($parent-selector, string-to-class($base)); // Nest class below the parent selector
    } @else if $scope {
        // Contains parent selector
        $base: selector.nest($scope, string-to-class($base));
    } @else if $pseudo {
        // Contains pseudo
        $base: selector.append(string-to-class($base), ':#{$pseudo}');
    } @else {
        // Contains nothing
        $base: string-to-class($base);
    }

    // Generate the class
    #{$base} {
        @content ($config);
    }
}

@mixin kv-class-generator($base-class-name, $class-value-pairs, $config: (), $variants: (), $context: ()) {
    @if $base-class-name != null {
        $new-base-class-name: '';
        @if string.length($base-class-name) > 0 {
            $new-base-class-name: $base-class-name + map.get($config, delimiter);
        }

        @each $key, $value in $class-value-pairs {
            $context: map.merge(
                $context,
                (
                    base-class-name: $new-base-class-name + $key,
                )
            );
            @include generate-rules-pseudos-selectors($config, $variants, $context) using ($props...) {
                @content ($key, $value, $props...);
            }
        }
    } @else {
        @include generate-rules-pseudos-selectors($config, $variants, $context) using ($props...) {
            @each $key, $value in $class-value-pairs {
                .#{$key} {
                    @include generate-rules-pseudos-selectors($config, $variants, $context) using ($props...) {
                        @content ($key, $value, $props...);
                    }
                }
            }
        }
    }
}

@mixin inline-class-generator($base-class-name, $config: (), $variants: (), $context: ()) {
    @if $base-class-name == null {
        @error 'Base class name must be provided';
    }

    $context: map.merge(
        $context,
        (
            base-class-name: $base-class-name,
        )
    );
    @include generate-rules-pseudos-selectors($config, $variants, $context) using ($props...) {
        @content ($props...);
    }
}

@function get-responsive() {
    $result: ();

    @each $key, $value in config.$breakpoint-pairs {
        $result: map.set($result, $key, '@media screen and (min-width: #{map.get(config.$breakpoints, $value)})');
    }

    @return $result;
}

@function get-dark() {
    @return ('dark': '@media (prefers-color-scheme: dark)');
}

@function get-light() {
    @return ('light': '@media (prefers-color-scheme: light)');
}

@function get-reduce-motion() {
    @return ('reduce-motion': '@media (prefers-reduced-motion: reduce)');
}

@function get-portrait() {
    @return ('portrait': '@media (orientation: portrait)');
}

@function get-landscape() {
    @return ('landscape': '@media (orientation: landscape)');
}

@function class-value-map-with-single-property($property, $property-values) {
    $result: ();

    @each $key, $value in $property-values {
        $result: map.set(
            $result,
            $key,
            (
                $property: $value,
            )
        );
    }

    @return $result;
}

/**
    * Convert a string to a proper class selector.
    * @param: {String}  $selector
    * @return: {Selector}  Returns the class selector.
    */
@function string-to-class($selector) {
    $separator: list.separator($selector);
    $result: ();

    @each $s in $selector {
        $selector-str: '#{$s}';
        @if $selector-str != '' {
            $result: list.append($result, '.#{$selector-str}', $separator);
        }
    }
    @return $result;
}

@function class-value-to-property-map($class-value-map, $property) {
    $result: ();
    @each $class, $value in $class-value-map {
        $result: map.set(
            $result,
            $class,
            (
                $property: $value,
            )
        );
    }
    @return $result;
}

/*
    Utility to use when generating classes with your own series of SCSS rules. This is typically used with the inline-class-generator mixin.

    @delimiter {string} [config.$delimiter] - delimiter used in class name body but not for separating psuedos. Can be empty.
    @variants {string[]} [()] - list of strings specifying which variants to generate styles for. Possible values:
        - 'responsive',
        - 'dark', 'light',
        - 'reduce-motion',
        - 'first-of-type',
        - 'last-of-type',
        - 'portrait', 'landscape',
        - 'hover', 'group-hover',
        - 'focus', 'group-focus', 'focus-visible', 'focus-within',
        - 'active',
        - 'visited',
        - 'checked',
        - 'disabled'

    @variant-delimiter {string} [config.$variant-delimiter] - delimiter used to separate the variant portion of the class. Can be empty but not advisable.
    @override {string} [config.$override] - override for CSS properties, like '!important'
*/
@mixin utility-with-body(
    $delimiter: config.$delimiter,
    $variants: (),
    $variant-delimiter: config.$variant-delimiter,
    $override: config.$override
) {
    $config: (
        delimiter: $delimiter,
        variant-delimiter: $variant-delimiter,
        override: $override,
    );
    @include generate-rules-pseudos-selectors(
            $config: $config,
            $variants: $variants,
            $context: (),
        )
        using ($props...) {
        @content ($props...);
    }
}
